namespace C4F.DevKit.Bluetooth.SerialPortProfile
{
    #region Using Directives
    using System;
    using System.Threading;
    using System.IO;
    using System.Net.Sockets;
    using System.IO.Ports;
    using System.Text;
    using C4F.DevKit.Bluetooth.SerialPortProfile.Resources;
    using System.ComponentModel;
    #endregion

    /// <summary>
    /// Implements Serial Port Profile.
    /// </summary>
    public class BluetoothSPPManager : Component
    {
        #region Private Fields
        /// <summary>
        /// Name of the port which will be used for communication.
        /// </summary>
        private string portName;

        /// <summary>
        /// SerialPort object representing the port which will be used for 
        /// communication.
        /// </summary>
        private SerialPort serialPort;

        /// <summary>
        /// NetworkStream which will be used for communication.
        /// </summary>
        private NetworkStream networkStream;

        /// <summary>
        /// Thread which will listen for DataDetectedFromRemoteDevice event.
        /// </summary>
        private Thread serialCommEventThread;

        #region Serial Port Settings

        /// <summary>
        /// Serial baud rate.
        /// </summary>
        private int baudRate = DEFAULT_BAUD_RATE;

        /// <summary>
        /// Standard length of data bits per byte.
        /// </summary>
        private int dataBits = DEFAULT_DATA_BITS;

        /// <summary>
        /// Parity-checking protocol. 
        /// </summary>
        private Parity parity = DEFAULT_PARITY;

        /// <summary>
        /// A byte that replaces invalid bytes in a data stream when a parity
        /// error occurs. 
        /// </summary>
        private byte parityReplace = DEFAULT_PARITY_REPLACE;

        /// <summary>
        /// The standard number of stopbits per byte. 
        /// </summary>
        private StopBits stopBits = DEFAULT_STOP_BITS;

        /// <summary>
        /// The size of the SerialPort input buffer. 
        /// </summary>
        private int readBufferSize = DEFAULT_READBUFFER_SIZE;

        /// <summary>
        /// The number of milliseconds before a time-out occurs when a read
        /// operation does not finish. 
        /// </summary>
        private int readTimeout = DEFAULT_READ_TIMEOUT;

        /// <summary>
        /// The size of the SerialPort output buffer. 
        /// </summary>
        private int writeBufferSize = DEFAULT_WRITEBUFFER_SIZE;

        /// <summary>
        /// The number of milliseconds before a time-out occurs when a write
        /// operation does not finish.
        /// </summary>
        private int writeTimeout = DEFAULT_WRITE_TIMEOUT;

        /// <summary>
        /// The byte encoding for pre- and post-transmission conversion of text.
        /// </summary>
        private Encoding encoding = ASCIIEncoding.ASCII;

        #endregion

        #endregion

        #region Private Constants

        /// <summary>
        /// The maximum length of data bits per byte.
        /// </summary>
        private const int MAX_DATA_BITS = 8;

        /// <summary>
        /// The minimum length of data bits per byte.
        /// </summary>
        private const int MIN_DATA_BITS = 5;

        #endregion

        #region Public Constants

        //Default values.

        /// <summary>
        /// The default value for serial baud rate is 9600 bits per second (bps).
        /// </summary>
        public const int DEFAULT_BAUD_RATE = 0x2580;

        /// <summary>
        /// The default value for standard length of data bits per byte is 8.
        /// </summary>
        public const int DEFAULT_DATA_BITS = 8;

        /// <summary>
        /// The default value for Parity-checking protocol is None.
        /// </summary>
        public const Parity DEFAULT_PARITY = Parity.None;

        /// <summary>
        /// The default value for the byte that replaces invalid bytes in a
        /// data stream when a parity error occurs is 0x3f.
        /// </summary>
        public const byte DEFAULT_PARITY_REPLACE = 0x3f;

        /// <summary>
        /// The default value for the standard number of stopbits per byte is 1.
        /// </summary>
        public const StopBits DEFAULT_STOP_BITS = StopBits.One;

        /// <summary>
        /// The default value for the size of the SerialPort input buffer is 
        /// 4096.
        /// </summary>
        public const int DEFAULT_READBUFFER_SIZE = 0x1000;

        /// <summary>
        /// The default value for the number of milliseconds before a time-out 
        /// occurs when a read operation does not finish is InfiniteTimeout.
        /// </summary>
        public const int DEFAULT_READ_TIMEOUT = -1;

        /// <summary>
        /// The default value for the size of the SerialPort output buffer is
        /// 2048.
        /// </summary>
        public const int DEFAULT_WRITEBUFFER_SIZE = 0x800;

        /// <summary>
        /// The default value for the number of milliseconds before a time-out
        /// occurs when a write operation does not finish is InfiniteTimeout.
        /// </summary>
        public const int DEFAULT_WRITE_TIMEOUT = -1;

        /// <summary>
        /// The value for InfiniteTimeout. 
        /// </summary>
        public const int INFINITE_TIMEOUT = -1;

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the name of the port.
        /// </summary>
        [Category(SPPConstants.Category_SerialPortSettings)]
        [Description(SPPConstants.Desc_PortName)]
        public string PortName
        {
            get { return this.portName; }
            set
            {
                if (this.serialPort != null)
                    throw new InvalidOperationException(SPPResources.Error_SP_PortSet);
                else { this.portName = value; }
            }
        }

        /// <summary>
        /// Gets the status of the port (Opened/Closed).
        /// </summary>
        [Browsable(false)]
        public bool IsPortOpen
        {
            get
            {
                if (this.serialPort == null)
                    return false;
                else return this.serialPort.IsOpen;
            }
        }

        /// <summary>
        /// Gets or sets the NetworkStream used for communicating with a remote device.
        /// </summary>
        [Browsable(false)]
        public NetworkStream NetworkStream
        {
            get
            { return this.networkStream; }
            set
            {
                this.networkStream = value;
                if (this.networkStream != null)
                {
                    this.networkStream.WriteTimeout = this.readTimeout;
                    this.networkStream.ReadTimeout = this.writeTimeout;
                }
            }
        }

        #region Serial Port Settings

        /// <summary>
        /// Gets or sets the serial baud rate. The default value is 9600 bits
        /// per second (bps).
        /// </summary>
        [Category(SPPConstants.Category_SerialPortSettings)]
        [Description(SPPConstants.Desc_BaudRate)]
        public int BaudRate
        {
            get
            { return this.baudRate; }
            set
            {
                if (value <= 0)
                    throw new ArgumentOutOfRangeException("BaudRate",
                        SPPResources.Error_SP_InvalidBaudRate);

                this.baudRate = value;
                if (this.serialPort != null)
                    this.serialPort.BaudRate = value;
            }
        }

        /// <summary>
        /// Gets or sets the standard length of data bits per byte. 
        /// The range of values for this field is from 5 through 8. 
        /// The default value is 8.
        /// </summary>
        [Category(SPPConstants.Category_SerialPortSettings)]
        [Description(SPPConstants.Desc_DataBits)]
        public int DataBits
        {
            get
            { return this.dataBits; }
            set
            {
                if ((value < MIN_DATA_BITS) || (value > MAX_DATA_BITS))
                    throw new ArgumentOutOfRangeException("DataBits",
                        SPPResources.Error_SP_DataBitsRange);

                this.dataBits = value;
                if (this.serialPort != null)
                    this.serialPort.DataBits = value;
            }
        }

        /// <summary>
        /// Gets or sets the byte encoding for pre- and post-transmission
        /// conversion of text. 
        /// The default is ASCIIEncoding.
        /// </summary>
        [Browsable(false)]
        public Encoding Encoding
        {
            get
            { return this.encoding; }
            set
            {
                if (value == null)
                    this.encoding = ASCIIEncoding.ASCII;
                else
                {
                    if (((!(value is ASCIIEncoding) &&
                        !(value is UTF8Encoding)) &&
                        (!(value is UnicodeEncoding) &&
                        !(value is UTF32Encoding))) &&
                        (((value.CodePage >= 0xc350)
                            && (value.CodePage != 0xd698))
                          || (value.GetType().Assembly !=
                                typeof(string).Assembly)))
                        throw new ArgumentException(
                            SPPResources.Error_SP_EncodingNotSupported, "Encoding");

                    this.encoding = value;
                    if (this.serialPort != null)
                        this.serialPort.Encoding = value;
                }
            }
        }

        /// <summary>
        /// Gets or sets the Parity-checking protocol. The default value is 
        /// None.
        /// </summary>
        [Category(SPPConstants.Category_SerialPortSettings)]
        [Description(SPPConstants.Desc_Parity)]
        public Parity Parity
        {
            get
            { return this.parity; }
            set
            {
                if (!Enum.IsDefined(typeof(Parity), value))
                    throw new ArgumentOutOfRangeException("Parity");

                this.parity = value;
                if (this.serialPort != null)
                    this.serialPort.Parity = value;
            }
        }

        /// <summary>
        /// Gets or sets the byte that replaces invalid bytes in a data stream 
        /// when a parity error occurs. 
        /// The default value is 0x3f.
        /// </summary>
        [Category(SPPConstants.Category_SerialPortSettings)]
        [Description(SPPConstants.Desc_ParityReplace)]
        public byte ParityReplace
        {
            get
            { return this.parityReplace; }
            set
            {
                this.parityReplace = value;
                if (this.serialPort != null)
                    this.serialPort.ParityReplace = value;
            }
        }

        /// <summary>
        /// Gets or sets the size of the SerialPort input buffer. The default
        /// value is 4096. If the port is open, ReadBufferSize will not be set.
        /// </summary>
        [Category(SPPConstants.Category_SerialPortSettings)]
        [Description(SPPConstants.Desc_ReadBufferSize)]
        public int ReadBufferSize
        {
            get
            { return this.readBufferSize; }
            set
            {
                if (value <= 0)
                    throw new ArgumentOutOfRangeException("ReadBufferSize");

                if (this.serialPort != null)
                    if (IsPortOpen)
                        // If the port is open, ReadBufferSize cannot be set. 
                        // So set the portsetting values to the current port settings.
                        this.readBufferSize = this.serialPort.ReadBufferSize;
                    else
                    {  // If the port is closed, ReadBufferSize can be set.
                        this.readBufferSize = value;
                        this.serialPort.ReadBufferSize = this.readBufferSize;
                    }
                else this.readBufferSize = value;
            }
        }

        /// <summary>
        /// Gets or sets the number of milliseconds before a time-out occurs
        /// when a read operation does not finish. 
        /// InfiniteTimeout is the default value.
        /// </summary>
        [Category(SPPConstants.Category_SerialPortSettings)]
        [Description(SPPConstants.Desc_ReadTimeout)]
        public int ReadTimeout
        {
            get
            { return this.readTimeout; }
            set
            {
                if ((value < 0) && (value != -1))
                    throw new ArgumentOutOfRangeException("ReadTimeout");

                this.readTimeout = value;
                if (this.serialPort != null)
                    this.serialPort.ReadTimeout = value;
                if (networkStream != null)
                    networkStream.WriteTimeout = this.readTimeout;
            }
        }

        /// <summary>
        /// Gets or sets the standard number of stopbits per byte. The default 
        /// value is 1.
        /// </summary>
        [Category(SPPConstants.Category_SerialPortSettings)]
        [Description(SPPConstants.Desc_StopBits)]
        public StopBits StopBits
        {
            get
            { return this.stopBits; }
            set
            {
                if (!Enum.IsDefined(typeof(StopBits), value))
                    throw new ArgumentOutOfRangeException("StopBits");

                this.stopBits = value;
                if (this.serialPort != null)
                    this.serialPort.StopBits = value;
            }
        }

        /// <summary>
        /// Gets or sets the size of the SerialPort output buffer. The default
        /// value is 2048. If the port is open, WriteBufferSize will not be set.
        /// </summary>
        [Category(SPPConstants.Category_SerialPortSettings)]
        [Description(SPPConstants.Desc_WriteBufferSize)]
        public int WriteBufferSize
        {
            get
            { return this.writeBufferSize; }
            set
            {
                if (value <= 0)
                    throw new ArgumentOutOfRangeException("WriteBufferSize");

                if (this.serialPort != null)
                    if (IsPortOpen)
                        // If the port is open, WriteBufferSize cannot be set. 
                        // So set the portsetting values to the current port settings.
                        this.writeBufferSize = this.serialPort.WriteBufferSize;
                    else
                    {  // If the port is closed, WriteBufferSize can be set.
                        this.writeBufferSize = value;
                        this.serialPort.WriteBufferSize = this.writeBufferSize;
                    }
                else this.writeBufferSize = value;
            }
        }

        /// <summary>
        /// Gets or sets the number of milliseconds before a time-out occurs
        /// when a write operation does not finish. 
        /// InfiniteTimeout is the default value.
        /// </summary>
        [Category(SPPConstants.Category_SerialPortSettings)]
        [Description(SPPConstants.Desc_WriteTimeout)]
        public int WriteTimeout
        {
            get
            { return this.writeTimeout; }
            set
            {
                if ((value <= 0) && (value != -1))
                    throw new ArgumentOutOfRangeException("WriteTimeout");

                this.writeTimeout = value;
                if (this.serialPort != null)
                    this.serialPort.WriteTimeout = value;
                if (networkStream != null)
                    networkStream.ReadTimeout = this.writeTimeout;
            }
        }
        
        #endregion

        #endregion

        #region Constructors and Destructors

        /// <summary>
        /// This constructor just creates a new SPPManager object. 
        /// </summary>
        public BluetoothSPPManager()
        { }

        ///// <summary>
        ///// This constructor takes port name and network stream as parameters 
        ///// and initializes all fields of this class.
        ///// </summary>
        ///// <param name="portName">Name of the port which will be used for 
        ///// communication.</param>
        ///// <param name="networkStream">NetworkStream which will be used for 
        ///// communication.</param>
        ///// <exception cref="ArgumentNullException">portName passed is an empty
        ///// string or a null reference or networkStream is a null reference.
        ///// </exception>
        //public SPPManager(string portName, NetworkStream networkStream)
        //{
        //    if (string.IsNullOrEmpty(portName) || string.IsNullOrEmpty(portName.Trim()))
        //        throw new ArgumentNullException("portName");

        //    if (networkStream == null)
        //        throw new ArgumentNullException("networkStream");

        //    // Set the port name and network stream.
        //    this.portName = portName;
        //    this.networkStream = networkStream;
        //}

        /// <summary>
        /// Destructor.
        /// </summary>
        ~BluetoothSPPManager()
        {
            if (this.serialPort != null)
            {
                this.serialPort.Close();
                this.serialPort.Dispose();
            }
            // If the thread that is listening to the network stream is alive,
            // abort it.
            if (this.serialCommEventThread != null)
                if (this.serialCommEventThread.IsAlive)
                    this.serialCommEventThread.Abort();
            this.Dispose(false);
        }

        #endregion

        #region Public Events and Delegates

        /// <summary>
        /// This represents the method that will be raised when data is detected
        /// on the port.
        /// </summary>
        /// <param name="sender">The sender of this event.</param>
        /// <param name="e">EventArgs object containing information about the 
        /// event.</param>
        public delegate void DataDetectedOnSerialPortEventHandler(
            object sender, EventArgs e);

        /// <summary>
        /// This represents the method that will be raised when data is detected
        /// on the NetworkStream.
        /// </summary>
        /// <param name="sender">The sender of this event.</param>
        /// <param name="e">EventArgs object containing information about the 
        /// event.</param>
        public delegate void DataDetectedFromRemoteDeviceEventHandler(
            object sender, EventArgs e);

        /// <summary>
        /// This event will be raised when incoming data is detected on the port
        /// (to be sent to the NetworkStream).
        /// </summary>
        public event DataDetectedOnSerialPortEventHandler
            DataDetectedOnSerialPort;

        /// <summary>
        /// This event will be raised when incoming data is detected on the 
        /// NetworkStream (to be sent to the port).
        /// </summary>
        public event DataDetectedFromRemoteDeviceEventHandler
            DataDetectedFromRemoteDevice;

        #endregion

        #region Private Functions/Methods

        /// <summary>
        /// Sets up the serial port.
        /// </summary>
        /// <exception cref="InvalidOperationException">PortName is not set.</exception>
        private void SetupSerialPort()
        {
            if (string.IsNullOrEmpty(this.portName) || string.IsNullOrEmpty(this.portName.Trim()))
                throw new InvalidOperationException(SPPResources.Error_SP_PortNameNotSet);

            // Set up the port.
            this.serialPort = new SerialPort(this.portName);
            this.serialPort.DataReceived +=
                new SerialDataReceivedEventHandler(serialPort_DataReceived);
            this.serialPort.ErrorReceived +=
                new SerialErrorReceivedEventHandler(serialPort_ErrorReceived);

            // Configure the port.
            ConfigurePort();
        }

        /// <summary>
        /// Configures the serial port settings. 
        /// Note that ReadBufferSize and WriteBufferSize will not be set if the
        /// port is open.
        /// </summary>
        /// <exception cref="ArgumentNullException">PortSettings property has 
        /// not been set.</exception>
        private void ConfigurePort()
        {
            serialPort.BaudRate = this.baudRate;
            serialPort.DataBits = this.dataBits;
            if (this.encoding != null)
                serialPort.Encoding = this.encoding;
            serialPort.Parity = this.parity;
            serialPort.ParityReplace = this.parityReplace;
            serialPort.StopBits = this.stopBits;
            serialPort.ReadTimeout = this.readTimeout;
            if (networkStream != null)
            {
                networkStream.WriteTimeout = this.readTimeout;
                networkStream.ReadTimeout = this.writeTimeout;
            }
            serialPort.WriteTimeout = this.writeTimeout;
            
            if (IsPortOpen)
            {
                // If the port is open, ReadBufferSize and WriteBufferSize 
                // cannot be set. So set the portsetting values to the 
                // current port settings.
                this.readBufferSize = serialPort.ReadBufferSize;
                this.writeBufferSize = serialPort.WriteBufferSize;
            }
            else
            {
                // If the port is closed, ReadBufferSize and WriteBufferSize
                // can be set.
                serialPort.ReadBufferSize = this.readBufferSize;
                serialPort.WriteBufferSize = this.writeBufferSize;
            }
        }

        /// <summary>
        /// Helps the serialCommEventThread to listen to the NetworkStream.
        /// If data is available, it raises a DataDetectedFromRemoteDevice 
        /// event.
        /// </summary>
        private void ReceiveThread()
        {
            while (this.IsPortOpen)
            {
                Thread.Sleep(100);
                // Raise an event that data is detected on the Network stream.
                if (this.networkStream.DataAvailable)
                    if (DataDetectedFromRemoteDevice != null)
                        DataDetectedFromRemoteDevice(this, new EventArgs());
            }
        }

        /// <summary>
        /// This is the handler method for the serial port when an error is
        /// received on the port. It raises an ErrorReceived event.
        /// </summary>
        /// <param name="sender">The sender of the event.</param>
        /// <param name="e">Provides data for the serial port's ErrorReceived
        /// event.</param>
        private void serialPort_ErrorReceived(
            object sender, SerialErrorReceivedEventArgs e)
        {
            string errorMessage = string.Empty;
            switch (e.EventType)
            {
                case SerialError.Frame:
                    errorMessage = SPPResources.Error_SP_Frame;
                    break;

                case SerialError.Overrun:
                    errorMessage = SPPResources.Error_SP_Overrun;
                    break;

                case SerialError.RXOver:
                    errorMessage = SPPResources.Error_SP_RXOver;
                    break;

                case SerialError.RXParity:
                    errorMessage = SPPResources.Error_SP_RXParity;
                    break;

                case SerialError.TXFull:
                    errorMessage = SPPResources.Error_SP_TXFull;
                    break;
            }
            throw new SPPException(
                SerialCommErrorType.SerialPortError, errorMessage);
        }

        /// <summary>
        /// This is the handler method for the serial port when data is received
        /// on the port. It raises a DataDetectedOnSerialPort event.
        /// </summary>
        /// <param name="sender">The sender of the event.</param>
        /// <param name="e">Provides data for the serial port's DataReceived
        /// event.</param>
        private void serialPort_DataReceived(
            object sender, SerialDataReceivedEventArgs e)
        {
            // Since data is detected on the serial port, raise an event.
            if (DataDetectedOnSerialPort != null)
                DataDetectedOnSerialPort(sender, new EventArgs());
        }

        #endregion

        #region Public Functions/Methods

        /// <summary>
        /// This method opens the specified port, configure it, and starts a 
        /// thread that listens to the NetworkStream.
        /// </summary>
        /// <exception cref="SPPException">An error occurs while opening the
        /// port. (Note that the inner exception contains details of the
        /// reason for exception)</exception>
        /// <exception cref="InvalidOperationException">networkStream is not set.</exception>
        public void OpenPort()
        {
            if (this.networkStream == null)
                throw new InvalidOperationException(SPPResources.Error_NS_NetworkStreamNotSet);

            try
            {
                if (this.serialPort == null)
                    // Setup the serial port using the specified port name.
                    SetupSerialPort();

                if (this.serialPort.IsOpen)
                    throw new InvalidOperationException(
                        SPPResources.Error_SP_PortOpen);

                //Open the port.
                this.serialPort.Open();

                //Start the thread that will listen to the NetworkStream.
                serialCommEventThread = new Thread(new ThreadStart(ReceiveThread));
                serialCommEventThread.Start();
            }
            catch (Exception ex)
            {
                throw new SPPException(SerialCommErrorType.SerialPortError,
                    SPPResources.Error_SP_OpenPort, ex);
            }
        }

        /// <summary>
        /// This method closes the open port.
        /// </summary>
        /// <exception cref="SPPException">An error occurs while closing the
        /// port. (Note that the inner exception contains details of the reason
        /// for exception)</exception>
        public void ClosePort()
        {
            try
            {
                if (this.serialPort == null || !this.serialPort.IsOpen)
                    throw new InvalidOperationException(
                        SPPResources.Error_SP_PortNotOpen);

                this.serialPort.Close();
            }
            catch (Exception ex)
            {
                throw new SPPException(SerialCommErrorType.SerialPortError,
                    SPPResources.Error_SP_ClosePort, ex);
            }
        }

        /// <summary>
        /// Reads numberOfBytesToSend bytes of data from the port and sends it
        /// on the NetworkStream. It returns the number of bytes actually sent
        /// to the remote device. The data that is sent is returned in the 
        /// output buffer.
        /// </summary>
        /// <param name="numberOfBytesToSend">Number of bytes to be sent to the
        /// remote device.</param>
        /// <param name="buffer">Output buffer to store the data that was sent 
        /// to the remote device.</param>
        /// <returns>Number of bytes actually sent to the remote device.
        /// </returns>
        /// <exception cref="ArgumentException">numberOfBytesToSend is less
        /// than zero or numberOfBytesToSend is greater than the serial port's
        /// ReadBufferSize.</exception>
        /// <exception cref="SPPException">An error occurs while sending data 
        /// to the remote device. (Note that the inner exception contains 
        /// details of the reason for exception)</exception>
        /// <exception cref="InvalidOperationException">networkStream is not set.</exception>
        public int SendDataToRemoteDevice(
            int numberOfBytesToSend, out byte[] buffer)
        {
            if (numberOfBytesToSend < 0)
                throw new ArgumentException(
                    SPPResources.Error_PositiveNo, "numberOfBytesToSend");

            if (numberOfBytesToSend > this.serialPort.ReadBufferSize)
                throw new ArgumentException(
                    SPPResources.Error_SP_numberOfBytesToSendToRemoteDevice,
                    "numberOfBytesToSend");

            if (this.serialPort == null || !this.serialPort.IsOpen)
                throw new SPPException(SerialCommErrorType.SerialPortError,
                    SPPResources.Error_SP_SendDataToRemoteDevice,
                    new InvalidOperationException(
                    SPPResources.Error_SP_PortNotOpen));

            if (this.networkStream == null)
                throw new InvalidOperationException(SPPResources.Error_NS_NetworkStreamNotSet);

            if (!this.networkStream.CanWrite)
                throw new SPPException(SerialCommErrorType.NetworkStreamError,
                    SPPResources.Error_SP_SendDataToRemoteDevice,
                    new InvalidOperationException(
                    SPPResources.Error_NS_ReadOnly));

            int numberOfBytesSent = 0;
            buffer = new byte[numberOfBytesToSend];

            // If no bytes have to be sent, return.
            if (numberOfBytesToSend == 0)
                return numberOfBytesSent;

            try
            {
                //Read data from the serial port into the buffer.
                numberOfBytesSent =
                    this.serialPort.Read(buffer, 0, numberOfBytesToSend);
            }
            catch (Exception ex)
            {
                throw new SPPException(SerialCommErrorType.SerialPortError,
                    SPPResources.Error_SP_SendDataToRemoteDevice, ex);
            }

            try
            {
                //Write the data from the buffer to the NetworkStream.
                this.networkStream.Write(buffer, 0, numberOfBytesSent);
            }
            catch (Exception ex)
            {
                throw new SPPException(SerialCommErrorType.NetworkStreamError,
                    SPPResources.Error_SP_SendDataToRemoteDevice, ex);
            }

            // Return the number of bytes sent to the remote device.
            return numberOfBytesSent;
        }

        /// <summary>
        /// Reads all the data present on the port and sends it on the Network
        /// Stream. It returns the number of bytes actually sent to the remote
        /// device. The data that is sent is returned in the output buffer.
        /// </summary>
        /// <param name="buffer">Output buffer to store the data that was sent 
        /// to the remote device.</param>
        /// <returns>Number of bytes actually sent to the remote device.
        /// </returns>
        /// <exception cref="SPPException">An error occurs while sending data
        /// to the remote device. (Note that the inner exception contains 
        /// details of the reason for exception)</exception>
        /// <exception cref="InvalidOperationException">networkStream is not set.</exception>
        public int SendDataToRemoteDevice(out byte[] buffer)
        {
            if (this.serialPort == null || !this.serialPort.IsOpen)
                throw new SPPException(SerialCommErrorType.SerialPortError,
                    SPPResources.Error_SP_SendDataToRemoteDevice,
                    new InvalidOperationException(
                    SPPResources.Error_SP_PortNotOpen));

            if (this.networkStream == null)
                throw new InvalidOperationException(SPPResources.Error_NS_NetworkStreamNotSet);

            if (!this.networkStream.CanWrite)
                throw new SPPException(SerialCommErrorType.NetworkStreamError,
                    SPPResources.Error_SP_SendDataToRemoteDevice,
                    new InvalidOperationException(
                    SPPResources.Error_NS_ReadOnly));

            int numberOfBytesSent = 0;

            try
            {
                //Read all existing data from the serial port into the buffer.
                buffer = Encoding.ASCII.GetBytes(this.serialPort.ReadExisting());
                numberOfBytesSent = buffer.Length;
            }
            catch (Exception ex)
            {
                throw new SPPException(SerialCommErrorType.SerialPortError,
                    SPPResources.Error_SP_SendDataToRemoteDevice, ex);
            }

            try
            {
                //Write the data from the buffer to the NetworkStream.
                this.networkStream.Write(buffer, 0, numberOfBytesSent);
            }
            catch (Exception ex)
            {
                throw new SPPException(SerialCommErrorType.NetworkStreamError,
                    SPPResources.Error_SP_SendDataToRemoteDevice, ex);
            }

            // Return the number of bytes sent to the remote device.
            return numberOfBytesSent;
        }

        /// <summary>
        /// Reads numberOfBytesToSend bytes of data from the NetworkStream and 
        /// sends it on the serial port. It returns the number of bytes actually
        /// sent to the serial port. The data that is sent is returned in the 
        /// output buffer.
        /// </summary>
        /// <param name="numberOfBytesToSend">Number of bytes to be sent to the 
        /// serial port.</param>
        /// <param name="buffer">Output buffer to store the data that was sent 
        /// to the serial port.</param>
        /// <returns>Number of bytes actually sent to the serial port.</returns>
        /// <exception cref="ArgumentException">numberOfBytesToSend is less
        /// than zero or numberOfBytesToSend is greater than the serial port's
        /// WriteBufferSize.</exception>
        /// <exception cref="SPPException">An error occurs while sending data
        /// to the serial port. (Note that the inner exception contains details
        /// of the reason for exception)</exception>
        /// <exception cref="InvalidOperationException">networkStream is not set.</exception>
        public int SendDataToSerialPort(
            int numberOfBytesToSend, out byte[] buffer)
        {
            if (numberOfBytesToSend < 0)
                throw new ArgumentException(
                    SPPResources.Error_PositiveNo, "numberOfBytesToSend");

            if (numberOfBytesToSend > this.serialPort.WriteBufferSize)
                throw new ArgumentException(
                    SPPResources.Error_SP_numberOfBytesToSendToSerialPort,
                    "numberOfBytesToSend");

            if (this.serialPort == null || !this.serialPort.IsOpen)
                throw new SPPException(SerialCommErrorType.SerialPortError,
                    SPPResources.Error_SP_SendDataToSerialPort,
                    new InvalidOperationException(
                    SPPResources.Error_SP_PortNotOpen));

            if (this.networkStream == null)
                throw new InvalidOperationException(SPPResources.Error_NS_NetworkStreamNotSet);

            if (!this.networkStream.CanRead)
                throw new SPPException(SerialCommErrorType.NetworkStreamError,
                    SPPResources.Error_SP_SendDataToSerialPort,
                    new InvalidOperationException(
                    SPPResources.Error_NS_WriteOnly));

            int numberOfBytesSent = 0;
            buffer = new byte[numberOfBytesToSend];

            // If no data has to be read, return.
            if (numberOfBytesToSend == 0)
                return numberOfBytesSent;

            try
            {
                //Read data from the NetworkStream into the buffer.
                numberOfBytesSent =
                    this.networkStream.Read(buffer, 0, numberOfBytesToSend);
            }
            catch (Exception ex)
            {
                throw new SPPException(SerialCommErrorType.NetworkStreamError,
                    SPPResources.Error_SP_SendDataToSerialPort, ex);
            }

            try
            {
                //Write data from the buffer to the port.
                this.serialPort.Write(buffer, 0, numberOfBytesSent);
            }
            catch (Exception ex)
            {
                throw new SPPException(SerialCommErrorType.SerialPortError,
                    SPPResources.Error_SP_SendDataToSerialPort, ex);
            }

            // Return the number of bytes sent to the serial port.
            return numberOfBytesSent;
        }

        /// <summary>
        /// Reads all the data present on the NetworkStream and sends it on the
        /// serial port. It returns the number of bytes actually sent to the 
        /// serial port. The data that is sent is returned in the output buffer.
        /// </summary>
        /// <param name="buffer">Output buffer to store the data that was sent 
        /// to the serial port.</param>
        /// <returns>Number of bytes actually sent to the serial port.</returns>
        /// <exception cref="SPPException">An error occurs while sending data
        /// to the serial port. (Note that the inner exception contains details
        /// of the reason for exception)</exception>
        /// <exception cref="InvalidOperationException">networkStream is not set.</exception>
        public int SendDataToSerialPort(out byte[] buffer)
        {
            if (this.serialPort == null || !this.serialPort.IsOpen)
                throw new SPPException(SerialCommErrorType.SerialPortError,
                    SPPResources.Error_SP_SendDataToSerialPort,
                    new InvalidOperationException(
                    SPPResources.Error_SP_PortNotOpen));

            if (this.networkStream == null)
                throw new InvalidOperationException(SPPResources.Error_NS_NetworkStreamNotSet);

            if (!this.networkStream.CanRead)
                throw new SPPException(SerialCommErrorType.NetworkStreamError,
                    SPPResources.Error_SP_SendDataToSerialPort,
                    new InvalidOperationException(
                    SPPResources.Error_NS_WriteOnly));

            int numberOfBytesSent = 0;

            MemoryStream dataToSend = new MemoryStream();
            int serialPortWriteBufferSize = this.serialPort.WriteBufferSize;

            try
            {
                while (this.networkStream.DataAvailable &&
                    dataToSend.Length < serialPortWriteBufferSize)
                {
                    // Read a byte from the NetworkStream into the MemoryStream 
                    // while there is data on the NetworkStream and the length 
                    // of the data read is less than the port buffer size.
                    int byteRead = this.networkStream.ReadByte();
                    if (byteRead == -1)
                        // If the byte read is -1, End of stream has been 
                        // encountered. Stop reading from the network stream.
                        break;
                    else
                        dataToSend.WriteByte((byte)byteRead);
                }
            }
            catch (Exception ex)
            {
                throw new SPPException(SerialCommErrorType.NetworkStreamError,
                    SPPResources.Error_SP_SendDataToSerialPort, ex);
            }

            //Put the data received from the NetworkStream into the buffer.
            buffer = dataToSend.ToArray();
            numberOfBytesSent = Convert.ToInt32(dataToSend.Length);
            dataToSend.Close();

            try
            {
                //Write the data in the buffer to the serial port.
                this.serialPort.Write(buffer, 0, numberOfBytesSent);
            }
            catch (Exception ex)
            {
                throw new SPPException(SerialCommErrorType.SerialPortError,
                    SPPResources.Error_SP_SendDataToSerialPort, ex);
            }
            return numberOfBytesSent;
        }

        #endregion
    }
}